/*-------------------------------------------------------------------------
 *
 * joinrels.c
 *	  确定哪些关系应被连接的例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/optimizer/path/joinrels.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "miscadmin.h"
#include "optimizer/appendinfo.h"
#include "optimizer/joininfo.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "partitioning/partbounds.h"
#include "utils/memutils.h"


static void fc_make_rels_by_clause_joins(PlannerInfo *fc_root,
									  RelOptInfo *fc_old_rel,
									  List *fc_other_rels_list,
									  ListCell *fc_other_rels);
static void fc_make_rels_by_clauseless_joins(PlannerInfo *fc_root,
										  RelOptInfo *fc_old_rel,
										  List *fc_other_rels);
static bool fc_has_join_restriction(PlannerInfo *fc_root, RelOptInfo *fc_rel);
static bool fc_has_legal_joinclause(PlannerInfo *fc_root, RelOptInfo *fc_rel);
static bool fc_restriction_is_constant_false(List *fc_restrictlist,
										  RelOptInfo *fc_joinrel,
										  bool fc_only_pushed_down);
static void fc_populate_joinrel_with_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel1,
										RelOptInfo *fc_rel2, RelOptInfo *fc_joinrel,
										SpecialJoinInfo *fc_sjinfo, List *fc_restrictlist);
static void fc_try_partitionwise_join(PlannerInfo *fc_root, RelOptInfo *fc_rel1,
								   RelOptInfo *fc_rel2, RelOptInfo *fc_joinrel,
								   SpecialJoinInfo *fc_parent_sjinfo,
								   List *fc_parent_restrictlist);
static SpecialJoinInfo *fc_build_child_join_sjinfo(PlannerInfo *fc_root,
												SpecialJoinInfo *fc_parent_sjinfo,
												Relids fc_left_relids, Relids fc_right_relids);
static void fc_compute_partition_bounds(PlannerInfo *fc_root, RelOptInfo *fc_rel1,
									 RelOptInfo *fc_rel2, RelOptInfo *fc_joinrel,
									 SpecialJoinInfo *fc_parent_sjinfo,
									 List **fc_parts1, List **fc_parts2);
static void get_matching_part_pairs(PlannerInfo *fc_root, RelOptInfo *fc_joinrel,
									RelOptInfo *fc_rel1, RelOptInfo *fc_rel2,
									List **fc_parts1, List **fc_parts2);



/*
 * join_search_one_level
 *	  考虑生成包含正好'level'个jointree项目的连接关系的方法。 
 *	  （这是动态规划方法的一步，体现在standard_join_search中。） 
 *	  为每个可行的较低级别关系组合创建连接关系节点，并将其返回在一个列表中。
 *	  也为每个这样的连接关系创建实现路径。
 *
 * level: 我们这次想要生成的关系级别
 * root->join_rel_level[j], 1 <= j < level，是一个包含j个项目的关系列表
 *
 * 结果返回在root->join_rel_level[level]中。
 */
void join_search_one_level(PlannerInfo *fc_root, int fc_level)
{
	List	  **fc_joinrels = fc_root->join_rel_level;
	ListCell   *fc_r;
	int			fc_k;

	Assert(fc_joinrels[fc_level] == NIL);

	/* 设置join_cur_level，以便将新的连接关系添加到适当的列表中 */
	fc_root->join_cur_level = fc_level;

	/*
	 * 首先，考虑左侧和右侧的计划，其中正好有level-1个成员关系的关系与初始关系连接。 
	 * 我们更喜欢使用连接子句进行连接，但如果我们发现某个级别-1成员的关系没有连接子句，我们将生成与所有未包含其中的初始关系的笛卡尔积连接。
	 */
	foreach(fc_r, fc_joinrels[fc_level - 1])
	{
		RelOptInfo *fc_old_rel = (RelOptInfo *) lfirst(fc_r);

		if (fc_old_rel->joininfo != NIL || fc_old_rel->has_eclass_joins ||
			fc_has_join_restriction(fc_root, fc_old_rel))
		{
			/*
			 * 这个关系有相关的连接子句或连接顺序限制，因此考虑该关系与（仅）通过子句或限制连接到的初始关系之间的连接。
			 *
			 * 在级别2时，这个条件是对称的，因此不需要查看列表中此之前的初始关系；我们已经
			 * 在早期的关系中考虑了这样的连接。 （镜像连接由make_join_rel自动处理。） 
			 * 在后续的遍历中（级别> 2），我们将较早级别的关系与每个尚未包含在内但有连接
			 * 子句或限制的初始关系连接。
			 */
			List	   *fc_other_rels_list;
			ListCell   *fc_other_rels;

			if (fc_level == 2)		/* 考虑剩余的初始关系 */
			{
				fc_other_rels_list = fc_joinrels[fc_level - 1];
				fc_other_rels = lnext(fc_other_rels_list, fc_r);
			}
			else				/* 考虑所有初始关系 */
			{
				fc_other_rels_list = fc_joinrels[1];
				fc_other_rels = list_head(fc_other_rels_list);
			}

			fc_make_rels_by_clause_joins(fc_root,
									  fc_old_rel,
									  fc_other_rels_list,
									  fc_other_rels);
		}
		else
		{
			/*
			 * 哎呀，我们有一个关系没有与任何其他关系直接或通过连接顺序限制连接。
			 * 笛卡尔积时间。
			 *
			 * 我们考虑与每个未包含的初始关系进行笛卡尔积，无论它是否有其他连接子句。
			 * 在级别2时，如果有两个或更多没有子句的初始关系，我们将冗余地考虑在两个方向上连接它们；
			 * 但这种情况并不常见，因此不值得增加复杂性来避免重复工作。
			 */
			fc_make_rels_by_clauseless_joins(fc_root,
										  fc_old_rel,
										  fc_joinrels[1]);
		}
	}

	/*
	 * 现在，考虑“灌木状计划”，其中k个初始关系的关系与级别-k的初始关系连接，
	 * 对于2 <= k <= level-2。
	 *
	 * 我们只考虑有合适连接子句（或连接顺序限制）的关系对的灌木状计划连接，以避免不合理的规划时间增长。
	 */
	for (fc_k = 2;; fc_k++)
	{
		int			fc_other_level = fc_level - fc_k;

		/*
		 * 由于make_join_rel(x, y)同时处理x,y和y,x情况，因此我们只需要走到中间点。
		 */
		if (fc_k > fc_other_level)
			break;

		foreach(fc_r, fc_joinrels[fc_k])
		{
			RelOptInfo *fc_old_rel = (RelOptInfo *) lfirst(fc_r);
			List	   *fc_other_rels_list;
			ListCell   *fc_other_rels;
			ListCell   *fc_r2;

			/*
			 * 我们可以在此处忽略没有连接子句的关系，除非它们参与连接顺序限制---那样我们可能
			 * 不得不强制进行一个灌木状连接计划。
			 */
			if (fc_old_rel->joininfo == NIL && !fc_old_rel->has_eclass_joins &&
				!fc_has_join_restriction(fc_root, fc_old_rel))
				continue;

			if (fc_k == fc_other_level)
			{
				/* 只考虑剩余的关系 */
				fc_other_rels_list = fc_joinrels[fc_k];
				fc_other_rels = lnext(fc_other_rels_list, fc_r);
			}
			else
			{
				fc_other_rels_list = fc_joinrels[fc_other_level];
				fc_other_rels = list_head(fc_other_rels_list);
			}

			for_each_cell(fc_r2, fc_other_rels_list, fc_other_rels)
			{
				RelOptInfo *fc_new_rel = (RelOptInfo *) lfirst(fc_r2);

				if (!bms_overlap(fc_old_rel->relids, fc_new_rel->relids))
				{
					/*
					 * 好的，我们可以从这对关系构建一个正确级别的关系。 如果有至少一个相关的
					 * 连接子句或连接顺序限制，则执行此操作。
					 */
					if (have_relevant_joinclause(fc_root, fc_old_rel, fc_new_rel) ||
						have_join_order_restriction(fc_root, fc_old_rel, fc_new_rel))
					{
						(void) make_join_rel(fc_root, fc_old_rel, fc_new_rel);
					}
				}
			}
		}
	}

	/*----------
	 * 最后的努力：如果到目前为止未能找到任何可用的连接，则强制生成一组笛卡尔积连接。 
	 * 这处理了所有可用关系都有连接子句但尚无法使用的特殊情况。 这种情况只会在我们
	 * 考虑一个连接子问题（一个子连接列表）时发生，并且子问题中的所有关系只有与子问题
	 * 外部的关系的连接子句。 一个例子是
	 *
	 *		SELECT ... FROM a INNER JOIN b ON TRUE, c, d, ...
	 *		WHERE a.w = c.x and b.y = d.z;
	 *
	 * 如果“a INNER JOIN b”子问题未被展平到上一级，我们必须愿意进行a和b的笛卡尔连接；
	 * 但上面的代码将不会这样做，因为它认为a和b都有连接子句。 在这种情况下我们只考虑
	 * 左侧和右侧的笛卡尔连接（没有灌木状连接）。
	 *----------
	 */
	if (fc_joinrels[fc_level] == NIL)
	{
		/*
		 * 这个循环与第一个完全相同，只是我们总是调用make_rels_by_clauseless_joins()。
		 */
		foreach(fc_r, fc_joinrels[fc_level - 1])
		{
			RelOptInfo *fc_old_rel = (RelOptInfo *) lfirst(fc_r);

			fc_make_rels_by_clauseless_joins(fc_root,
										  fc_old_rel,
										  fc_joinrels[1]);
		}

		/*----------
		 * 当涉及特殊连接时，对于某些 N 值，可能没有合法的方法
		 * 进行 N 对连接。例如考虑
		 *
		 * SELECT ... FROM t1 WHERE
		 *	 x IN (SELECT ... FROM t2,t3 WHERE ...) AND
		 *	 y IN (SELECT ... FROM t4,t5 WHERE ...)
		 *
		 * 我们将把这个查询扁平化为一个 5 对连接的问题，但没有 4 对连接
		 * 的 join_is_legal() 会被认为是合法的。我们不得不在第 4 层接受失败，并继续在第 5 层发现一个可行的繁茂计划。
		 *
		 * 然而，如果没有特殊连接且没有横向引用，那么 join_is_legal() 应该永远不会失败，因此以下的完整性检查是有用的。
		 *----------
		 */
		if (fc_joinrels[fc_level] == NIL &&
			fc_root->join_info_list == NIL &&
			!fc_root->hasLateralRTEs)
			elog(ERROR, "failed to build any %d-way joins", fc_level);
	}
}

/*
 * make_rels_by_clause_joins
 *	  在给定的关系 'old_rel' 和其他参与连接子句的关系之间建立连接
 *	  'old_rel' 也参与的连接子句
 *	  （或与它一起参与连接顺序限制）。
 *	  连接关系返回在 root->join_rel_level[join_cur_level] 中。
 *
 * 注意：在高于 2 的级别时，我们将在多种方式中生成相同的连接关系——例如 (a join b) join c 与
 * (b join c) join a 是相同的 RelOptInfo，尽管第二种情况会为其添加不同的路径集合。
 * 这就是使用 join_rel_level 机制的原因，它自动确保每个新连接关系仅添加到列表中一次。
 *
 * 'old_rel' 是要连接的关系的关系项
 * 'other_rels_list'：一个包含其他
 * 要考虑连接的关系的列表
 * 'other_rels'：要考虑的首个单元
 *
 * 当前，这仅与其他关系中的初始关系一起使用，但它
 * 也将适用于连接到连接关系。
 */
static void fc_make_rels_by_clause_joins(PlannerInfo *fc_root,
						  RelOptInfo *fc_old_rel,
						  List *fc_other_rels_list,
						  ListCell *fc_other_rels)
{
	ListCell   *fc_l;

	for_each_cell(fc_l, fc_other_rels_list, fc_other_rels)
	{
		RelOptInfo *fc_other_rel = (RelOptInfo *) lfirst(fc_l);

		if (!bms_overlap(fc_old_rel->relids, fc_other_rel->relids) &&
			(have_relevant_joinclause(fc_root, fc_old_rel, fc_other_rel) ||
			 have_join_order_restriction(fc_root, fc_old_rel, fc_other_rel)))
		{
			(void) make_join_rel(fc_root, fc_old_rel, fc_other_rel);
		}
	}
}

/*
 * make_rels_by_clauseless_joins
 *	  给定关系 'old_rel' 和其他关系的列表
 *	  'other_rels'，在 'old_rel' 和
 *	  'other_rels' 中每个不是已包含在 'old_rel' 中的成员之间创建一个连接关系。
 *	  连接关系返回在 root->join_rel_level[join_cur_level] 中。
 *
 * 'old_rel' 是要连接的关系的关系项
 * 'other_rels'：一个包含要考虑连接的其他关系的列表
 *
 * 当前，这仅与其他关系中的初始关系一起使用，但它也可以
 * 用于连接到连接关系。
 */
static void fc_make_rels_by_clauseless_joins(PlannerInfo *fc_root,
							  RelOptInfo *fc_old_rel,
							  List *fc_other_rels)
{
	ListCell   *fc_l;

	foreach(fc_l, fc_other_rels)
	{
		RelOptInfo *fc_other_rel = (RelOptInfo *) lfirst(fc_l);

		if (!bms_overlap(fc_other_rel->relids, fc_old_rel->relids))
		{
			(void) make_join_rel(fc_root, fc_old_rel, fc_other_rel);
		}
	}
}


/*
 * join_is_legal
 *	   确定在给定查询的连接顺序约束下，建议的连接是否合法；如果合法，确定连接类型。
 *
 * 调用者必须提供两个关系，以及它们关系ID的并集。
 * （我们可以通过在本地计算joinrelids来简化API，但这在正常路径中是多余的工作。）
 *
 * 成功时，*sjinfo_p 如果这是一个普通的内部连接，则设置为 NULL，
 * 否则设置为指向相关的 SpecialJoinInfo 节点。 此外，
 * 如果给定关系需要交换以匹配 SpecialJoinInfo 节点，则 *reversed_p 设置为 true。
 */
static bool fc_join_is_legal(PlannerInfo *fc_root, RelOptInfo *fc_rel1, RelOptInfo *fc_rel2,
			  Relids fc_joinrelids,
			  SpecialJoinInfo **fc_sjinfo_p, bool *fc_reversed_p)
{
	SpecialJoinInfo *fc_match_sjinfo;
	bool		fc_reversed;
	bool		fc_unique_ified;
	bool		fc_must_be_leftjoin;
	ListCell   *fc_l;

	/*
	 * 确保在失败返回时输出参数已设置。这只是为了
	 * 压制过于严格的编译器的未初始化变量警告。
	 */
	*fc_sjinfo_p = NULL;
	*fc_reversed_p = false;

	/*
	 * 如果我们有任何特殊连接，建议的连接可能不合法；在任何情况下，我们都必须确定其连接类型。
	 * 扫描连接信息列表以查找匹配和冲突。
	 */
	fc_match_sjinfo = NULL;
	fc_reversed = false;
	fc_unique_ified = false;
	fc_must_be_leftjoin = false;

	foreach(fc_l, fc_root->join_info_list)
	{
		SpecialJoinInfo *fc_sjinfo = (SpecialJoinInfo *) lfirst(fc_l);

		/*
		 * 该特殊连接只有在其右侧重叠建议的连接时才相关。
		 * （首先检查这一点，以便快速排除大多数不相关的 SJs。）
		 */
		if (!bms_overlap(fc_sjinfo->min_righthand, fc_joinrelids))
			continue;

		/*
		 * 如果建议的连接完全包含在右侧中（即，我们仍在构建右侧），
		 * 则也不相关。
		 */
		if (bms_is_subset(fc_joinrelids, fc_sjinfo->min_righthand))
			continue;

		/*
		 * 如果 SJ 已在任一输入中完成，则也不相关。
		 */
		if (bms_is_subset(fc_sjinfo->min_lefthand, fc_rel1->relids) &&
			bms_is_subset(fc_sjinfo->min_righthand, fc_rel1->relids))
			continue;
		if (bms_is_subset(fc_sjinfo->min_lefthand, fc_rel2->relids) &&
			bms_is_subset(fc_sjinfo->min_righthand, fc_rel2->relids))
			continue;

		/*
		 * 如果这是一个半连接，并且我们已经将右侧连接到任一输入中的任何其他关系，
		 * 那么我们必须在那时唯一化右侧（见下文）。
		 * 因此，该半连接在此连接路径中不再相关。
		 */
		if (fc_sjinfo->jointype == JOIN_SEMI)
		{
			if (bms_is_subset(fc_sjinfo->syn_righthand, fc_rel1->relids) &&
				!bms_equal(fc_sjinfo->syn_righthand, fc_rel1->relids))
				continue;
			if (bms_is_subset(fc_sjinfo->syn_righthand, fc_rel2->relids) &&
				!bms_equal(fc_sjinfo->syn_righthand, fc_rel2->relids))
				continue;
		}

		/*
		 * 如果一个输入包含 min_lefthand 而另一个包含
		 * min_righthand，那么我们可以在此连接执行 SJ。
		 *
		 * 如果我们找到多个 SJ 的匹配，则拒绝；这意味着我们正在考虑一些实际上
		 * 并不合法的东西。
		 */
		if (bms_is_subset(fc_sjinfo->min_lefthand, fc_rel1->relids) &&
			bms_is_subset(fc_sjinfo->min_righthand, fc_rel2->relids))
		{
			if (fc_match_sjinfo)
				return false;	/* 无效的连接路径 */
			fc_match_sjinfo = fc_sjinfo;
			fc_reversed = false;
		}
		else if (bms_is_subset(fc_sjinfo->min_lefthand, fc_rel2->relids) &&
				 bms_is_subset(fc_sjinfo->min_righthand, fc_rel1->relids))
		{
			if (fc_match_sjinfo)
				return false;	/* 无效的连接路径 */
			fc_match_sjinfo = fc_sjinfo;
			fc_reversed = true;
		}
		else if (fc_sjinfo->jointype == JOIN_SEMI &&
				 bms_equal(fc_sjinfo->syn_righthand, fc_rel2->relids) &&
				 create_unique_path(fc_root, fc_rel2, fc_rel2->cheapest_total_path,
									fc_sjinfo) != NULL)
		{
			/*----------
			 * 对于半连接，我们可以通过唯一化右侧将右侧连接到任何其他关系。
			 * 我们只会在拥有完整右侧但左侧小于 min_lefthand 时到达这里。
			 *
			 * 考虑这种连接路径的原因通过以下示例说明：
			 *	SELECT ... FROM a,b WHERE (a.x,b.y) IN (SELECT c1,c2 FROM c)
			 * 如果我们坚持将其作为半连接进行处理，我们将首先形成 A*B 的笛卡尔积。
			 * 但是如果我们唯一化 C，那么半连接就变成了普通的内部连接，
			 * 我们可以按任意顺序连接，例如首先连接 C 到 A，然后连接到 B。
			 * 当 C 的大小远小于 A 和 B 时，这可以带来巨大的好处。
			 * 因此，我们允许 C 在此仅连接到 A 或仅连接到 B，
			 * 然后 make_join_rel 必须正确处理这种情况。
			 *
			 * 请注意，实际上我们还会允许唯一化的 C 连接到
			 * 其他关系 D。这是合法的，尽管通常并不理智，
			 * 而该例程仅关注合法性而不关心连接是否是好的策略。
			 *----------
			 */
			if (fc_match_sjinfo)
				return false;	/* 无效的连接路径 */
			fc_match_sjinfo = fc_sjinfo;
			fc_reversed = false;
			fc_unique_ified = true;
		}
		else if (fc_sjinfo->jointype == JOIN_SEMI &&
				 bms_equal(fc_sjinfo->syn_righthand, fc_rel1->relids) &&
				 create_unique_path(fc_root, fc_rel1, fc_rel1->cheapest_total_path,
									fc_sjinfo) != NULL)
		{
			/* 反向半连接情况 */
			if (fc_match_sjinfo)
				return false;	/* 无效的连接路径 */
			fc_match_sjinfo = fc_sjinfo;
			fc_reversed = true;
			fc_unique_ified = true;
		}
		else
		{
			/*
			 * 另一方面，建议的连接与右侧重叠，但不是这一 SJ 的有效实现。
			 * 但不要太紧张：右侧违规可能在之前的某个时刻发生于一个或两个输入关系中，
			 * 在这种情况下，我们必须之前决定将某个其他 SJ 与此 SJ 进行交换是可以的。
			 * 如果我们需要执行此连接以完成构建右侧，拒绝它可能导致根本无法找到任何计划。
			 * （这可能是因为此文件中的其他启发式方法推迟了无子句的连接：
			 * 在执行其他有效的可交换 SJ 之前，我们可能不会考虑在右侧进行无子句连接。）
			 * 这个考虑归结为这样一个规则：如果两个输入都与右侧重叠，我们就可以允许连接---要么
			 * 完全在右侧内，要么表示之前允许的与外部关系的连接。
			 */
			if (bms_overlap(fc_rel1->relids, fc_sjinfo->min_righthand) &&
				bms_overlap(fc_rel2->relids, fc_sjinfo->min_righthand))
				continue;		/* 假设右侧先前的违规是有效的 */

			/*
			 * 建议的连接仍然可能是合法的，但前提是我们被允许将其关联到这一 SJ 的右侧。
			 * 这意味着这一 SJ 必须是左连接（不是半连接或反连接，当然也不是全连接），
			 * 并且建议的连接不得与左侧重叠。
			 */
			if (fc_sjinfo->jointype != JOIN_LEFT ||
				bms_overlap(fc_joinrelids, fc_sjinfo->min_lefthand))
				return false;	/* 无效的连接路径 */

			/*
			 * 为了有效，建议的连接必须是左连接；否则，
			 * 它无法关联到这一 SJ 的右侧。
			 * 但我们可能还没有找到与建议连接匹配的 SpecialJoinInfo，
			 * 所以暂时无法测试这一点。请记住稍后的要求。
			 */
			fc_must_be_leftjoin = true;
		}
	}

	
/*
	 * 如果违反了任何SJ的右侧并且没有匹配到左侧SJ，则失败：拟议的连接不能与SJ的右侧关联。
	 *
	 * 同样，如果拟议连接的谓词不是严格的，则失败；我们基本上是在检查是否可以应用外连接身份3，这是一项要求。 （此检查可能与make_outerjoininfo中的检查冗余，但我不太确定，而且测试成本低。）
	 */
	if (fc_must_be_leftjoin &&
		(fc_match_sjinfo == NULL ||
		 fc_match_sjinfo->jointype != JOIN_LEFT ||
		 !fc_match_sjinfo->lhs_strict))
		return false;			/* 无效的连接路径 */

	/*
	 * 我们还必须检查由LATERAL引用施加的约束。
	 */
	if (fc_root->hasLateralRTEs)
	{
		bool		fc_lateral_fwd;
		bool		fc_lateral_rev;
		Relids		fc_join_lateral_rels;

		/*
		 * 拟议的关系可能各自包含对另一个的lateral引用，在这种情况下，连接是不可能的。如果只在一个方向上有lateral引用，那么必须使用nestloop进行连接，lateral引用者在内部。如果连接匹配一个不能通过这样的nestloop实现的SJ，则连接是不可能的。
		 *
		 * 此外，如果lateral引用仅仅是间接的，我们应该拒绝连接；无论引用链通过哪个关系，都必须首先连接。
		 *
		 * 另一个可能阻止我们建立有效计划的情况是have_dangerous_phv()描述的实现限制。
		 */
		fc_lateral_fwd = bms_overlap(fc_rel1->relids, fc_rel2->lateral_relids);
		fc_lateral_rev = bms_overlap(fc_rel2->relids, fc_rel1->lateral_relids);
		if (fc_lateral_fwd && fc_lateral_rev)
			return false;		/* 在两个方向上都有lateral引用 */
		if (fc_lateral_fwd)
		{
			/* 必须实现为左侧为rel1的nestloop */
			if (fc_match_sjinfo &&
				(fc_reversed ||
				 fc_unique_ified ||
				 fc_match_sjinfo->jointype == JOIN_FULL))
				return false;	/* 不能作为nestloop实现 */
			/* 检查rel2到rel1是否有直接引用 */
			if (!bms_overlap(fc_rel1->relids, fc_rel2->direct_lateral_relids))
				return false;	/* 只有间接引用，因此拒绝 */
			/* 检查我们不会有危险的PHV */
			if (have_dangerous_phv(fc_root, fc_rel1->relids, fc_rel2->lateral_relids))
				return false;	/* 可能无法处理所需的PHV */
		}
		else if (fc_lateral_rev)
		{
			/* 必须实现为左侧为rel2的nestloop */
			if (fc_match_sjinfo &&
				(!fc_reversed ||
				 fc_unique_ified ||
				 fc_match_sjinfo->jointype == JOIN_FULL))
				return false;	/* 不能作为nestloop实现 */
			/* 检查rel1到rel2是否有直接引用 */
			if (!bms_overlap(fc_rel2->relids, fc_rel1->direct_lateral_relids))
				return false;	/* 只有间接引用，因此拒绝 */
			/* 检查我们不会有危险的PHV */
			if (have_dangerous_phv(fc_root, fc_rel2->relids, fc_rel1->lateral_relids))
				return false;	/* 可能无法处理所需的PHV */
		}

		/*
		 * 如果我们接受这个连接，LATERAL引用也可能在以后引起问题：如果连接的最小参数化包括任何必须在与这个连接关系的外连接内部的关系，那么使用这个连接构建完整查询将永远不可能。我们应该拒绝这个连接，不仅因为这将节省工作，而且因为如果我们不这样做，未带子句的连接启发式方法可能认为这个连接的合法性意味着某些其他连接关系不必形成，这可能导致无法找到任何计划。我们必须考虑不仅与连接关系直接在外连接内部的关系，还要考虑间接在内的关系，因此需要进行搜索以查找所有此类关系。
		 */
		fc_join_lateral_rels = min_join_parameterization(fc_root, fc_joinrelids,
													  fc_rel1, fc_rel2);
		if (fc_join_lateral_rels)
		{
			Relids		fc_join_plus_rhs = bms_copy(fc_joinrelids);
			bool		fc_more;

			do
			{
				fc_more = false;
				foreach(fc_l, fc_root->join_info_list)
				{
					SpecialJoinInfo *fc_sjinfo = (SpecialJoinInfo *) lfirst(fc_l);

					/* 忽略完整连接 --- 其顺序是预先确定的 */
					if (fc_sjinfo->jointype == JOIN_FULL)
						continue;

					if (bms_overlap(fc_sjinfo->min_lefthand, fc_join_plus_rhs) &&
						!bms_is_subset(fc_sjinfo->min_righthand, fc_join_plus_rhs))
					{
						fc_join_plus_rhs = bms_add_members(fc_join_plus_rhs,
														fc_sjinfo->min_righthand);
						fc_more = true;
					}
				}
			} while (fc_more);
			if (bms_overlap(fc_join_plus_rhs, fc_join_lateral_rels))
				return false;	/* 将无法连接到某些右侧关系 */
		}
	}

	/* 否则，它是一个有效的连接 */
	*fc_sjinfo_p = fc_match_sjinfo;
	*fc_reversed_p = fc_reversed;
	return true;
}


/*
 * make_join_rel
 *	   查找或创建一个表示两个给定关系连接的 RelOptInfo，并为
 *	   使用两个关系作为外部和内部关系创建的路径添加路径信息。
 *	   （连接关系可能已经包含从其他关系对生成的路径，这些路径加起来
 *	   构成相同的基本关系集。）
 *
 * 注意：如果尝试的连接无效，将返回 NULL。这可能发生在处理外部连接或
 * 被转化为连接的 IN 或 EXISTS 子句时。
 */
RelOptInfo * make_join_rel(PlannerInfo *fc_root, RelOptInfo *fc_rel1, RelOptInfo *fc_rel2)
{
	Relids		fc_joinrelids;
	SpecialJoinInfo *fc_sjinfo;
	bool		fc_reversed;
	SpecialJoinInfo fc_sjinfo_data;
	RelOptInfo *fc_joinrel;
	List	   *fc_restrictlist;

	/* 我们绝不应尝试连接两个重叠的关系集。 */
	Assert(!bms_overlap(fc_rel1->relids, fc_rel2->relids));

	/* 构造标识连接关系的 Relids 集合。 */
	fc_joinrelids = bms_union(fc_rel1->relids, fc_rel2->relids);

	/* 检查有效性并确定连接类型。 */
	if (!fc_join_is_legal(fc_root, fc_rel1, fc_rel2, fc_joinrelids,
					   &fc_sjinfo, &fc_reversed))
	{
		/* 无效的连接路径 */
		bms_free(fc_joinrelids);
		return NULL;
	}

	/* 如有必要，交换关系以匹配连接信息。 */
	if (fc_reversed)
	{
		RelOptInfo *fc_trel = fc_rel1;

		fc_rel1 = fc_rel2;
		fc_rel2 = fc_trel;
	}

	/*
	 * 如果这是一个普通的内部连接，那么我们在 join_info_list 中
	 * 不会找到任何东西。构建一个 SpecialJoinInfo，这样选择性
	 * 估算函数就会知道正在连接什么。
	 */
	if (fc_sjinfo == NULL)
	{
		fc_sjinfo = &fc_sjinfo_data;
		fc_sjinfo->type = T_SpecialJoinInfo;
		fc_sjinfo->min_lefthand = fc_rel1->relids;
		fc_sjinfo->min_righthand = fc_rel2->relids;
		fc_sjinfo->syn_lefthand = fc_rel1->relids;
		fc_sjinfo->syn_righthand = fc_rel2->relids;
		fc_sjinfo->jointype = JOIN_INNER;
		/* 我们不打算尝试使剩余字段有效 */
		fc_sjinfo->lhs_strict = false;
		fc_sjinfo->delay_upper_joins = false;
		fc_sjinfo->semi_can_btree = false;
		fc_sjinfo->semi_can_hash = false;
		fc_sjinfo->semi_operators = NIL;
		fc_sjinfo->semi_rhs_exprs = NIL;
	}

	/*
	 * 查找或构建连接 RelOptInfo，并计算与此特定连接相对应的 
	 * restrictlist。
	 */
	fc_joinrel = build_join_rel(fc_root, fc_joinrelids, fc_rel1, fc_rel2, fc_sjinfo,
							 &fc_restrictlist);

	/*
	 * 如果我们已经证明这个连接是空的，我们就不需要考虑任何
	 * 其他路径。
	 */
	if (is_dummy_rel(fc_joinrel))
	{
		bms_free(fc_joinrelids);
		return fc_joinrel;
	}

	/* 向连接关系中添加路径。 */
	fc_populate_joinrel_with_paths(fc_root, fc_rel1, fc_rel2, fc_joinrel, fc_sjinfo,
								fc_restrictlist);

	bms_free(fc_joinrelids);

	return fc_joinrel;
}

/*
 * populate_joinrel_with_paths
 *	  为给定一对连接关系向给定的连接关系添加路径。
 *	  SpecialJoinInfo 提供了连接的详细信息，而 restrictlist
 *	  包含连接子句和适用于给定连接关系对的其他子句。
 */
static void fc_populate_joinrel_with_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel1,
							RelOptInfo *fc_rel2, RelOptInfo *fc_joinrel,
							SpecialJoinInfo *fc_sjinfo, List *fc_restrictlist)
{
	/*
	 * 考虑使用每个关系作为外部和内部的路径。根据连接类型,
	 * 一个可证明是空的外部或内部关系可能意味着连接也可证明为空；
	 * 在这种情况下，抛弃任何先前计算的路径并将连接标记为临时。
	 * （我们以这种方式处理是因为可以想象，多个元素连接的临时性
	 * 可能仅在某些构造路径中显著。）
	 *
	 * 此外，一个可证明的恒定假连接限制通常意味着我们可以跳过
	 * 评估连接的一侧或两侧。我们通过将适当的关系标记为临时来实现。
	 * 对于外部连接，一个推进的恒定假限制仍然意味着整个连接是临时的，
	 * 而一个未推进的限制则意味着没有内部行会连接，因此我们可以
	 * 将内部关系视为临时。
	 *
	 * 我们仅需考虑在 join_info_list 中出现的连接类型，以及
	 * JOIN_INNER。
	 */
	switch (fc_sjinfo->jointype)
	{
		case JOIN_INNER:
			if (is_dummy_rel(fc_rel1) || is_dummy_rel(fc_rel2) ||
				fc_restriction_is_constant_false(fc_restrictlist, fc_joinrel, false))
			{
				mark_dummy_rel(fc_joinrel);
				break;
			}
			add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel1, fc_rel2,
								 JOIN_INNER, fc_sjinfo,
								 fc_restrictlist);
			add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel2, fc_rel1,
								 JOIN_INNER, fc_sjinfo,
								 fc_restrictlist);
			break;
		case JOIN_LEFT:
			if (is_dummy_rel(fc_rel1) ||
				fc_restriction_is_constant_false(fc_restrictlist, fc_joinrel, true))
			{
				mark_dummy_rel(fc_joinrel);
				break;
			}
			if (fc_restriction_is_constant_false(fc_restrictlist, fc_joinrel, false) &&
				bms_is_subset(fc_rel2->relids, fc_sjinfo->syn_righthand))
				mark_dummy_rel(fc_rel2);
			add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel1, fc_rel2,
								 JOIN_LEFT, fc_sjinfo,
								 fc_restrictlist);
			add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel2, fc_rel1,
								 JOIN_RIGHT, fc_sjinfo,
								 fc_restrictlist);
			break;
		case JOIN_FULL:
			if ((is_dummy_rel(fc_rel1) && is_dummy_rel(fc_rel2)) ||
				fc_restriction_is_constant_false(fc_restrictlist, fc_joinrel, true))
			{
				mark_dummy_rel(fc_joinrel);
				break;
			}
			add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel1, fc_rel2,
								 JOIN_FULL, fc_sjinfo,
								 fc_restrictlist);
			add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel2, fc_rel1,
								 JOIN_FULL, fc_sjinfo,
								 fc_restrictlist);

			/*
			 * 如果有一些连接资格不能合并或哈希，我们
			 * 可能无法构建任何有效的计划。在这里抱怨，以便我们
			 * 能提供一个相对有用的错误信息。（由于我们在完全连接中的
			 * 规划没有灵活性，因此后面以其他输入关系成功的机会
			 * 微乎其微。）
			 */
			if (fc_joinrel->pathlist == NIL)
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("FULL JOIN is only supported with merge-joinable or hash-joinable join conditions")));
			break;
		case JOIN_SEMI:

			/*
			 * 我们可能有一个正常的半连接，或者一种情况，即我们没有
			 * 足够的关系来进行半连接，但可以对 RHS 进行唯一化，然后
			 * 进行内部连接（请参阅 join_is_legal 中的注释）。在后者的
			 * 情况下，我们无法应用 JOIN_SEMI 连接。
			 */
			if (bms_is_subset(fc_sjinfo->min_lefthand, fc_rel1->relids) &&
				bms_is_subset(fc_sjinfo->min_righthand, fc_rel2->relids))
			{
				if (is_dummy_rel(fc_rel1) || is_dummy_rel(fc_rel2) ||
					fc_restriction_is_constant_false(fc_restrictlist, fc_joinrel, false))
				{
					mark_dummy_rel(fc_joinrel);
					break;
				}
				add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel1, fc_rel2,
									 JOIN_SEMI, fc_sjinfo,
									 fc_restrictlist);
			}

			/*
			 * 如果我们知道如何对 RHS 进行唯一化，并且一个输入关系
			 * 恰好是 RHS（不是超集），我们可以考虑对其进行唯一化
			 * 然后进行常规连接。（这里的 create_unique_path
			 * 检查可能与 join_is_legal 做的重复，但如果是这样，检查
			 * 是便宜的，因为它被缓存。因此无论如何进行测试以确保。）
			 */
			if (bms_equal(fc_sjinfo->syn_righthand, fc_rel2->relids) &&
				create_unique_path(fc_root, fc_rel2, fc_rel2->cheapest_total_path,
								   fc_sjinfo) != NULL)
			{
				if (is_dummy_rel(fc_rel1) || is_dummy_rel(fc_rel2) ||
					fc_restriction_is_constant_false(fc_restrictlist, fc_joinrel, false))
				{
					mark_dummy_rel(fc_joinrel);
					break;
				}
				add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel1, fc_rel2,
									 JOIN_UNIQUE_INNER, fc_sjinfo,
									 fc_restrictlist);
				add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel2, fc_rel1,
									 JOIN_UNIQUE_OUTER, fc_sjinfo,
									 fc_restrictlist);
			}
			break;
		case JOIN_ANTI:
			if (is_dummy_rel(fc_rel1) ||
				fc_restriction_is_constant_false(fc_restrictlist, fc_joinrel, true))
			{
				mark_dummy_rel(fc_joinrel);
				break;
			}
			if (fc_restriction_is_constant_false(fc_restrictlist, fc_joinrel, false) &&
				bms_is_subset(fc_rel2->relids, fc_sjinfo->syn_righthand))
				mark_dummy_rel(fc_rel2);
			add_paths_to_joinrel(fc_root, fc_joinrel, fc_rel1, fc_rel2,
								 JOIN_ANTI, fc_sjinfo,
								 fc_restrictlist);
			break;
		default:
			/* 此处不期望其他值 */
			elog(ERROR, "unrecognized join type: %d", (int) fc_sjinfo->jointype);
			break;
	}

	/* 如果可能，应用分区连接技术。 */
	fc_try_partitionwise_join(fc_root, fc_rel1, fc_rel2, fc_joinrel, fc_sjinfo, fc_restrictlist);
}


/*
 * have_join_order_restriction
 *		检测两个关系是否应该连接以满足因特殊或侧边连接而产生的
 *		连接顺序限制。
 *
 * 在实践中，这总是与 have_relevant_joinclause() 一起使用，
 * 因此可以与该函数合并，但将这两个问题分开似乎更清晰。
 * 我们需要这个测试，因为在某些退化情况下，必须进行无子句
 * 的连接以满足连接顺序限制。此外，如果一个关系有对另一个关系
 * 的侧边引用，或者两个关系都需要计算某些 PHV，我们应该考虑连接
 * 这两个关系，即使连接将是无子句的。
 *
 * 注意：只有在退化外连接的一侧包含多个关系，或者在 IN/EXISTS RHS 中需要
 * 无子句连接时，这才是一个问题；否则我们将通过 join_search_one_level()
 * 中的“最后一搏”情况找到连接路径。如果我们愿意在“最后一搏”情况下尝试
 * bushy 计划，我们可以放弃此测试，但这看起来要低效得多。
 */
bool have_join_order_restriction(PlannerInfo *fc_root,
							RelOptInfo *fc_rel1, RelOptInfo *fc_rel2)
{
	bool		fc_result = false;
	ListCell   *fc_l;

	/*
	 * 如果任一侧直接参考另一个侧点，则无论外部连接的考虑
	 * 如何，尝试连接。
	 */
	if (bms_overlap(fc_rel1->relids, fc_rel2->direct_lateral_relids) ||
		bms_overlap(fc_rel2->relids, fc_rel1->direct_lateral_relids))
		return true;

	
/*
	 * 同样，如果两个关系都需要来计算某个 PlaceHolderVar，
	 * 尝试进行连接，而不考虑外连接的因素。 （这并不是
	 * 非常理想，因为带有大 eval_at 集的 PHV 会导致考虑很多
	 * 可能无用的连接，但不这样做可能会导致我们根本无法构建计划。）
	 */
	foreach(fc_l, fc_root->placeholder_list)
	{
		PlaceHolderInfo *fc_phinfo = (PlaceHolderInfo *) lfirst(fc_l);

		if (bms_is_subset(fc_rel1->relids, fc_phinfo->ph_eval_at) &&
			bms_is_subset(fc_rel2->relids, fc_phinfo->ph_eval_at))
			return true;
	}

	/*
	 * 关系可能对应于一 个退化外连接的左侧和右侧，即没有提到
	 * 非空侧的连接条件；在这种情况下，我们应该强制进行连接。
	 *
	 * 此外，这两个关系可能表示需要完成的无子句连接，以
	 * 构建外连接的 LHS 或 RHS。
	 */
	foreach(fc_l, fc_root->join_info_list)
	{
		SpecialJoinInfo *fc_sjinfo = (SpecialJoinInfo *) lfirst(fc_l);

		/* 忽略完全连接 --- 其他机制处理它们 */
		if (fc_sjinfo->jointype == JOIN_FULL)
			continue;

		/* 我们能用这些关系执行 SJ 吗？ */
		if (bms_is_subset(fc_sjinfo->min_lefthand, fc_rel1->relids) &&
			bms_is_subset(fc_sjinfo->min_righthand, fc_rel2->relids))
		{
			fc_result = true;
			break;
		}
		if (bms_is_subset(fc_sjinfo->min_lefthand, fc_rel2->relids) &&
			bms_is_subset(fc_sjinfo->min_righthand, fc_rel1->relids))
		{
			fc_result = true;
			break;
		}

		/*
		 * 我们是否需要连接这些关系以完成 RHS？我们必须
		 * 使用“重叠”测试，因为其中任何一个关系可能包括一个
		 * 已证明可以与此连接交换的位置的下层 SJ。
		 */
		if (bms_overlap(fc_sjinfo->min_righthand, fc_rel1->relids) &&
			bms_overlap(fc_sjinfo->min_righthand, fc_rel2->relids))
		{
			fc_result = true;
			break;
		}

		/* LHS 也是如此。 */
		if (bms_overlap(fc_sjinfo->min_lefthand, fc_rel1->relids) &&
			bms_overlap(fc_sjinfo->min_lefthand, fc_rel2->relids))
		{
			fc_result = true;
			break;
		}
	}

	/*
	 * 如果任一输入关系可以合法地与其他任何东西通过连接条件连接，
	 * 我们不强制进行连接。这基本上意味着无子句蓬松连接会尽可能延迟。
	 * 原因是，当连接树的高层有连接顺序限制时
	 * （即，LHS 或 RHS 内有许多关系），否则我们会
	 * 消耗大量精力考虑其 LHS 或 RHS 中非常愚蠢的连接组合。
	 */
	if (fc_result)
	{
		if (fc_has_legal_joinclause(fc_root, fc_rel1) ||
			fc_has_legal_joinclause(fc_root, fc_rel2))
			fc_result = false;
	}

	return fc_result;
}


/*
 * has_join_restriction
 *		检测指定关系是否有连接顺序限制，
 *		由于在外连接、IN（子选择）内，
 *		或参与任何 LATERAL 引用或多关系 PHVs。
 *
 * 从本质上讲，这测试是否有 join_order_restriction() 能
 * 与此关系和另一个关系一起成功。 如果我们有时错误地
 * 说“真”，那也没关系。 （因此，我们不在乎相对昂贵的
 * has_legal_joinclause 测试。）
 */
static bool fc_has_join_restriction(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	ListCell   *fc_l;

	if (fc_rel->lateral_relids != NULL || fc_rel->lateral_referencers != NULL)
		return true;

	foreach(fc_l, fc_root->placeholder_list)
	{
		PlaceHolderInfo *fc_phinfo = (PlaceHolderInfo *) lfirst(fc_l);

		if (bms_is_subset(fc_rel->relids, fc_phinfo->ph_eval_at) &&
			!bms_equal(fc_rel->relids, fc_phinfo->ph_eval_at))
			return true;
	}

	foreach(fc_l, fc_root->join_info_list)
	{
		SpecialJoinInfo *fc_sjinfo = (SpecialJoinInfo *) lfirst(fc_l);

		/* 忽略完全连接 --- 其他机制保持其顺序 */
		if (fc_sjinfo->jointype == JOIN_FULL)
			continue;

		/* 如果 SJ 已经包含在关系中则忽略 */
		if (bms_is_subset(fc_sjinfo->min_lefthand, fc_rel->relids) &&
			bms_is_subset(fc_sjinfo->min_righthand, fc_rel->relids))
			continue;

		/* 如果它与 LHS 或 RHS 重叠，但不包含 SJ 则受限 */
		if (bms_overlap(fc_sjinfo->min_lefthand, fc_rel->relids) ||
			bms_overlap(fc_sjinfo->min_righthand, fc_rel->relids))
			return true;
	}

	return false;
}


/*
 * has_legal_joinclause
 *		检测指定关系是否可以合法地通过连接条件
 *		与任何其他关系连接。
 *
 * 我们仅考虑与当前初始关系列表中的单个其他关系的连接。
 * 这在大多数实际查询中足以得到“真”结果，
 * 偶尔出现的错误“假”只会多花一点规划时间。
 * 这样限制的原因是，考虑到与其他连接的连接需要证明
 * 其他连接关系可以合法形成，这似乎对只是一种
 * 节省规划时间的启发式方法来说成本太高。
 * （注意：我们必须查看初始关系，而不是查询的所有内容，
 * 因为当我们在规划子连接列表时，我们可能被迫在初始关系内
 * 进行无子句连接，即使有连接条件链接到查询的其他部分。）
 */
static bool fc_has_legal_joinclause(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	ListCell   *fc_lc;

	foreach(fc_lc, fc_root->initial_rels)
	{
		RelOptInfo *fc_rel2 = (RelOptInfo *) lfirst(fc_lc);

		/* 忽略已经在 "rel" 中的关系 */
		if (bms_overlap(fc_rel->relids, fc_rel2->relids))
			continue;

		if (have_relevant_joinclause(fc_root, fc_rel, fc_rel2))
		{
			Relids		fc_joinrelids;
			SpecialJoinInfo *fc_sjinfo;
			bool		fc_reversed;

			/* join_is_legal 需要联合的 relids */
			fc_joinrelids = bms_union(fc_rel->relids, fc_rel2->relids);

			if (fc_join_is_legal(fc_root, fc_rel, fc_rel2, fc_joinrelids,
							  &fc_sjinfo, &fc_reversed))
			{
				/* 是的，这样可以工作 */
				bms_free(fc_joinrelids);
				return true;
			}

			bms_free(fc_joinrelids);
		}
	}

	return false;
}


/*
 * 创建参数化的 nestloops 时有一个陷阱：假设内部
 * rel（称之为 A）有一个参数是 PlaceHolderVar，并且该 PHV 的
 * 最小 eval_at 集合包括外部 rel（B）和某个第三个 rel（C）。
 * 我们可能认为可以创建一个参数化为 C 的 B/A nestloop 连接。
 * 但是我们最终会得到一个计划，其中 PHV 的表达式必须在
 * B/A 连接处被评估为 nestloop 参数；而执行器只被设置为处理简单
 * Vars 作为 NestLoopParams。为了避免为这种特殊情况增加复杂性
 * 和开销，禁止该连接似乎是更好的选择。（请注意，我们仍然可以
 * 使用 A 的参数化路径，前提是联合的 B+C 作为外部 rel。 have_join_order_restriction()
 * 确保即使没有其他原因，我们也会考虑进行这样的连接。）
 *
 * 因此，我们检查查询中使用的任何 PHVs 是否可能构成这样的危险。
 * 我们没有简单的方法来检查风险 PHV 是否会实际在内部计划中被
 * 使用，而且这种情况是如此不寻常，以至于似乎不值得为其付出
 * 太多努力。
 *
 * 这需要在两个地方检查。如果内部 rel 的最小
 * 参数化会触发限制，则 join_is_legal() 应该
 * 完全拒绝该连接，因为没有可行的路径。
 * 但是 joinpath.c 必须针对每个拟议的 nestloop 路径再次进行检查，
 * 因为内部路径可能有超过最小参数化，从而导致某些 PHV 对其
 * 却是危险的。
 */
bool have_dangerous_phv(PlannerInfo *fc_root,
				   Relids fc_outer_relids, Relids fc_inner_params)
{
	ListCell   *fc_lc;

	foreach(fc_lc, fc_root->placeholder_list)
	{
		PlaceHolderInfo *fc_phinfo = (PlaceHolderInfo *) lfirst(fc_lc);

		if (!bms_is_subset(fc_phinfo->ph_eval_at, fc_inner_params))
			continue;			/* 忽略，可能不是 nestloop 参数 */
		if (!bms_overlap(fc_phinfo->ph_eval_at, fc_outer_relids))
			continue;			/* 忽略，与此连接无关 */
		if (bms_is_subset(fc_phinfo->ph_eval_at, fc_outer_relids))
			continue;			/* 安全，可以在 outerrel 内部进行评估 */
		/* 否则，它可能不安全，因此拒绝连接 */
		return true;
	}

	/* 可以执行连接 */
	return false;
}


/*
 * is_dummy_rel --- 已经证明关系为空了吗？
 */
bool is_dummy_rel(RelOptInfo *fc_rel)
{
	Path	   *fc_path;

	/*
	 * 一个已知的虚拟关系将只有一条没有子节点的路径
	 * Append。（即使它以某种方式拥有更多的路径，一个没有子节点的
	 * Append 将具有零成本，因此应该在路径列表的前面。）
	 */
	if (fc_rel->pathlist == NIL)
		return false;
	fc_path = (Path *) linitial(fc_rel->pathlist);

	/*
	 * 最初，一个虚拟路径将只是一个没有子节点的 Append。
	 * 但是在后续的规划阶段，我们可能会在其上方添加一个
	 * ProjectSetPath 和/或 ProjectionPath，因为 Append 不能进行投影。
	 * 为了避免对可能出现的组合做出假设，只需遍历我们找到的任何内容。
	 */
	for (;;)
	{
		if (IsA(fc_path, ProjectionPath))
			fc_path = ((ProjectionPath *) fc_path)->subpath;
		else if (IsA(fc_path, ProjectSetPath))
			fc_path = ((ProjectSetPath *) fc_path)->subpath;
		else
			break;
	}
	if (IS_DUMMY_APPEND(fc_path))
		return true;
	return false;
}

/*
 * 将一个关系标记为已证明为空。
 *
 * 在 GEQO 规划期间，这可能会在同一个 baserel 结构上调用多次，
 * 所以值得检查一下 rel 是否已被标记为虚拟。
 *
 * 此外，当在 GEQO 加入规划期间调用时，我们处于一个短暂的
 * 内存上下文中。我们必须确保附加到 baserel 的虚拟路径在
 * GEQO 循环中生存，否则 baserel 会被损坏，无法用于未来的
 * GEQO 循环。另一方面，当我们在 GEQO 中标记一个 joinrel 时，
 * 我们不希望虚拟路径干扰主要规划上下文。最终的结果
 * 是，最佳解决方案是在给定的 RelOptInfo 所在的相同上下文中
 * 显式创建虚拟路径。
 */
void mark_dummy_rel(RelOptInfo *fc_rel)
{
	MemoryContext fc_oldcontext;

	/* 已经标记了吗？ */
	if (is_dummy_rel(fc_rel))
		return;

	/* 不，所以选择正确的上下文来创建虚拟路径 */
	fc_oldcontext = MemoryContextSwitchTo(GetMemoryChunkContext(fc_rel));

	/* 设置虚拟大小估计 */
	fc_rel->rows = 0;

	/* 驱逐任何先前选择的路径 */
	fc_rel->pathlist = NIL;
	fc_rel->partial_pathlist = NIL;

	/* 设置虚拟路径 */
	add_path(fc_rel, (Path *) create_append_path(NULL, fc_rel, NIL, NIL,
											  NIL, fc_rel->lateral_relids,
											  0, false, -1));

	/* 设置或更新 cheapest_total_path 和相关字段 */
	set_cheapest(fc_rel);

	MemoryContextSwitchTo(fc_oldcontext);
}


/*
 * restriction_is_constant_false --- restrictlist是否只是 FALSE？
 *
 * 在可证明常量为 FALSE 的情况下，eval_const_expressions
 * 通常会抛弃与之 AND 的任何内容。在外部连接的情况下，
 * 这将导致我们计算笛卡尔乘积，只是为了发现没有
 * 与外层行匹配，这非常愚蠢。因此，我们需要检测这种情况。
 *
 * 如果 only_pushed_down 为真，则只考虑从连接关系的
 * 角度推进的 quals。
 */
static bool fc_restriction_is_constant_false(List *fc_restrictlist,
							  RelOptInfo *fc_joinrel,
							  bool fc_only_pushed_down)
{
	ListCell   *fc_lc;

	/*
	 * 尽管上述评论如此，我们在这里看到的限制列表可能
	 * 可能除了 FALSE 常量外，还有其他成员，因为其他
	 * quals 可能被“推向”外部连接级别。因此我们检查
	 * 列表的每个成员。
	 */
	foreach(fc_lc, fc_restrictlist)
	{
		RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_lc);

		if (fc_only_pushed_down && !RINFO_IS_PUSHED_DOWN(fc_rinfo, fc_joinrel->relids))
			continue;

		if (fc_rinfo->clause && IsA(fc_rinfo->clause, Const))
		{
			Const	   *fc_con = (Const *) fc_rinfo->clause;

			/* 常量 NULL 对我们来说和常量 FALSE 一样好 */
			if (fc_con->constisnull)
				return true;
			if (!DatumGetBool(fc_con->constvalue))
				return true;
		}
	}
	return false;
}


/*
 * 评估给定的两个分区关系之间的连接是否可以分解为
 * 匹配分区之间的连接；这一技术称为
 * "分区连接"
 *
 * 当以下条件满足时，可以进行分区连接：a. 连接关系具有相同的
 * 分区方案 b. 存在两个关系的分区键之间的等值连接。
 *
 * 分区连接的计划如下（详情见：optimizer/README.）
 *
 * 1. 为匹配分区之间的连接创建 RelOptInfos，即
 * 子连接并为其添加路径。
 *
 * 2. 跨一组子连接构造 Append 或 MergeAppend 路径。
 * 第二个阶段由 generate_partitionwise_join_paths() 实现。
 *
 * 每个子连接的 RelOptInfo、SpecialJoinInfo 和 restrictlist
 * 通过翻译相应的父连接结构来获得。
 */
static void fc_try_partitionwise_join(PlannerInfo *fc_root, RelOptInfo *fc_rel1, RelOptInfo *fc_rel2,
					   RelOptInfo *fc_joinrel, SpecialJoinInfo *fc_parent_sjinfo,
					   List *fc_parent_restrictlist)
{
	bool		fc_rel1_is_simple = IS_SIMPLE_REL(fc_rel1);
	bool		fc_rel2_is_simple = IS_SIMPLE_REL(fc_rel2);
	List	   *fc_parts1 = NIL;
	List	   *fc_parts2 = NIL;
	ListCell   *fc_lcr1 = NULL;
	ListCell   *fc_lcr2 = NULL;
	int			fc_cnt_parts;

	/* 防止由于过深的分区层次结构导致堆栈溢出。 */
	check_stack_depth();

	/* 如果连接关系未分区，则无需操作。 */
	if (fc_joinrel->part_scheme == NULL || fc_joinrel->nparts == 0)
		return;

	/* 连接关系应该设置 consider_partitionwise_join。 */
	Assert(fc_joinrel->consider_partitionwise_join);

	/*
	 * 如果任一连接关系未分区，则无法执行分区连接。
	 */
	if (!IS_PARTITIONED_REL(fc_rel1) || !IS_PARTITIONED_REL(fc_rel2))
		return;

	Assert(REL_HAS_ALL_PART_PROPS(fc_rel1) && REL_HAS_ALL_PART_PROPS(fc_rel2));

	/* 连接关系应该设置 consider_partitionwise_join。 */
	Assert(fc_rel1->consider_partitionwise_join &&
		   fc_rel2->consider_partitionwise_join);

	/*
	 * 连接关系的分区方案应该与连接关系的分区方案匹配。
	 */
	Assert(fc_joinrel->part_scheme == fc_rel1->part_scheme &&
		   fc_joinrel->part_scheme == fc_rel2->part_scheme);

	Assert(!(fc_joinrel->partbounds_merged && (fc_joinrel->nparts <= 0)));

	fc_compute_partition_bounds(fc_root, fc_rel1, fc_rel2, fc_joinrel, fc_parent_sjinfo,
							 &fc_parts1, &fc_parts2);

	if (fc_joinrel->partbounds_merged)
	{
		fc_lcr1 = list_head(fc_parts1);
		fc_lcr2 = list_head(fc_parts2);
	}

	/*
	 * 如果不存在，则为此分区连接创建子连接关系。为一对
	 * 对应于给定一对父关系的子关系添加子连接路径。
	 */
	for (fc_cnt_parts = 0; fc_cnt_parts < fc_joinrel->nparts; fc_cnt_parts++)
	{
		RelOptInfo *fc_child_rel1;
		RelOptInfo *fc_child_rel2;
		bool		fc_rel1_empty;
		bool		fc_rel2_empty;
		SpecialJoinInfo *fc_child_sjinfo;
		List	   *fc_child_restrictlist;
		RelOptInfo *fc_child_joinrel;
		Relids		fc_child_joinrelids;
		AppendRelInfo **fc_appinfos;
		int			fc_nappinfos;

		if (fc_joinrel->partbounds_merged)
		{
			fc_child_rel1 = lfirst_node(RelOptInfo, fc_lcr1);
			fc_child_rel2 = lfirst_node(RelOptInfo, fc_lcr2);
			fc_lcr1 = lnext(fc_parts1, fc_lcr1);
			fc_lcr2 = lnext(fc_parts2, fc_lcr2);
		}
		else
		{
			fc_child_rel1 = fc_rel1->part_rels[fc_cnt_parts];
			fc_child_rel2 = fc_rel2->part_rels[fc_cnt_parts];
		}

		fc_rel1_empty = (fc_child_rel1 == NULL || IS_DUMMY_REL(fc_child_rel1));
		fc_rel2_empty = (fc_child_rel2 == NULL || IS_DUMMY_REL(fc_child_rel2));

		/*
		 * 检查我们可以证明此连接段返回零行的情况，
		 * 原因是一个或两个输入为空（包括
		 * 已完全删除的输入）。如果是这样则忽略它。
		 * 这些规则等同于 populate_joinrel_with_paths 的规则
		 * 用于虚拟输入关系。
		 */
		switch (fc_parent_sjinfo->jointype)
		{
			case JOIN_INNER:
			case JOIN_SEMI:
				if (fc_rel1_empty || fc_rel2_empty)
					continue;	/* 忽略此连接段 */
				break;
			case JOIN_LEFT:
			case JOIN_ANTI:
				if (fc_rel1_empty)
					continue;	/* 忽略此连接段 */
				break;
			case JOIN_FULL:
				if (fc_rel1_empty && fc_rel2_empty)
					continue;	/* 忽略此连接段 */
				break;
			default:
				/* 此处不期望其他值 */
				elog(ERROR, "unrecognized join type: %d",
					 (int) fc_parent_sjinfo->jointype);
				break;
		}

		/*
		 * 如果一个子节点完全被剪枝，那么我们无法为其生成路径，
		 * 因此，除非我们能够在上层消除此分区，否则必须拒绝分区连接。
		 */
		if (fc_child_rel1 == NULL || fc_child_rel2 == NULL)
		{
			/*
			 * 将连接关系标记为非分区，以便后续函数正确处理。
			 */
			fc_joinrel->nparts = 0;
			return;
		}

		/*
		 * 如果一个叶关系的 consider_partitionwise_join=false，意味着
		 * 这是一个虚拟关系，我们跳过了设置 tlist 表达式和在 set_append_rel_size()
		 * 中添加 EC 成员，因此我们在这里必须失败。
		 */
		if (fc_rel1_is_simple && !fc_child_rel1->consider_partitionwise_join)
		{
			Assert(fc_child_rel1->reloptkind == RELOPT_OTHER_MEMBER_REL);
			Assert(IS_DUMMY_REL(fc_child_rel1));
			fc_joinrel->nparts = 0;
			return;
		}
		if (fc_rel2_is_simple && !fc_child_rel2->consider_partitionwise_join)
		{
			Assert(fc_child_rel2->reloptkind == RELOPT_OTHER_MEMBER_REL);
			Assert(IS_DUMMY_REL(fc_child_rel2));
			fc_joinrel->nparts = 0;
			return;
		}

		/* 我们绝不应尝试连接两个重叠的关系集。 */
		Assert(!bms_overlap(fc_child_rel1->relids, fc_child_rel2->relids));
		fc_child_joinrelids = bms_union(fc_child_rel1->relids, fc_child_rel2->relids);
		fc_appinfos = find_appinfos_by_relids(fc_root, fc_child_joinrelids, &fc_nappinfos);

		/*
		 * 从父连接关系的 SpecialJoinInfo 构造 SpecialJoinInfo。
		 */
		fc_child_sjinfo = fc_build_child_join_sjinfo(fc_root, fc_parent_sjinfo,
											   fc_child_rel1->relids,
											   fc_child_rel2->relids);

		/*
		 * 从适用于父连接的限制构造适用于子连接的限制。
		 */
		fc_child_restrictlist =
			(List *) adjust_appendrel_attrs(fc_root,
											(Node *) fc_parent_restrictlist,
											fc_nappinfos, fc_appinfos);
		pfree(fc_appinfos);

		fc_child_joinrel = fc_joinrel->part_rels[fc_cnt_parts];
		if (!fc_child_joinrel)
		{
			fc_child_joinrel = build_child_join_rel(fc_root, fc_child_rel1, fc_child_rel2,
												 fc_joinrel, fc_child_restrictlist,
												 fc_child_sjinfo,
												 fc_child_sjinfo->jointype);
			fc_joinrel->part_rels[fc_cnt_parts] = fc_child_joinrel;
			fc_joinrel->live_parts = bms_add_member(fc_joinrel->live_parts, fc_cnt_parts);
			fc_joinrel->all_partrels = bms_add_members(fc_joinrel->all_partrels,
													fc_child_joinrel->relids);
		}

		Assert(bms_equal(fc_child_joinrel->relids, fc_child_joinrelids));

		fc_populate_joinrel_with_paths(fc_root, fc_child_rel1, fc_child_rel2,
									fc_child_joinrel, fc_child_sjinfo,
									fc_child_restrictlist);
	}
}

/*
 * 通过转换父连接之间的 SpecialJoinInfo 来为子连接构造 SpecialJoinInfo。
 * left_relids 和 right_relids 分别是连接的左右两侧的 relids。
 */
static SpecialJoinInfo * fc_build_child_join_sjinfo(PlannerInfo *fc_root, SpecialJoinInfo *fc_parent_sjinfo,
						Relids fc_left_relids, Relids fc_right_relids)
{
	SpecialJoinInfo *fc_sjinfo = makeNode(SpecialJoinInfo);
	AppendRelInfo **fc_left_appinfos;
	int			fc_left_nappinfos;
	AppendRelInfo **fc_right_appinfos;
	int			fc_right_nappinfos;

	memcpy(fc_sjinfo, fc_parent_sjinfo, sizeof(SpecialJoinInfo));
	fc_left_appinfos = find_appinfos_by_relids(fc_root, fc_left_relids,
											&fc_left_nappinfos);
	fc_right_appinfos = find_appinfos_by_relids(fc_root, fc_right_relids,
											 &fc_right_nappinfos);

	fc_sjinfo->min_lefthand = adjust_child_relids(fc_sjinfo->min_lefthand,
											   fc_left_nappinfos, fc_left_appinfos);
	fc_sjinfo->min_righthand = adjust_child_relids(fc_sjinfo->min_righthand,
												fc_right_nappinfos,
												fc_right_appinfos);
	fc_sjinfo->syn_lefthand = adjust_child_relids(fc_sjinfo->syn_lefthand,
											   fc_left_nappinfos, fc_left_appinfos);
	fc_sjinfo->syn_righthand = adjust_child_relids(fc_sjinfo->syn_righthand,
												fc_right_nappinfos,
												fc_right_appinfos);
	fc_sjinfo->semi_rhs_exprs = (List *) adjust_appendrel_attrs(fc_root,
															 (Node *) fc_sjinfo->semi_rhs_exprs,
															 fc_right_nappinfos,
															 fc_right_appinfos);

	pfree(fc_left_appinfos);
	pfree(fc_right_appinfos);

	return fc_sjinfo;
}

/*
 * compute_partition_bounds
 *		计算连接关系的分区边界，基于输入的分区边界。
 */
static void fc_compute_partition_bounds(PlannerInfo *fc_root, RelOptInfo *fc_rel1,
						 RelOptInfo *fc_rel2, RelOptInfo *fc_joinrel,
						 SpecialJoinInfo *fc_parent_sjinfo,
						 List **fc_parts1, List **fc_parts2)
{
	/*
	 * 如果还没有连接关系的分区边界，尝试与待连接的分区对一起计算这些边界。
	 */
	if (fc_joinrel->nparts == -1)
	{
		PartitionScheme fc_part_scheme = fc_joinrel->part_scheme;
		PartitionBoundInfo fc_boundinfo = NULL;
		int			fc_nparts = 0;

		Assert(fc_joinrel->boundinfo == NULL);
		Assert(fc_joinrel->part_rels == NULL);

		/*
		 * 查看输入的分区边界是否完全相同，如果是的话，我们就不需要费太大力气：
		 * 连接关系将与输入具有相同的分区边界，具有相同基数位置的分区将构成配对。
		 *
		 * 注意：即使在一个或两个输入合并边界的情况下，两个边界可能完全相同，
		 * 但检查的周期似乎并不值得。
		 */
		if (!fc_rel1->partbounds_merged &&
			!fc_rel2->partbounds_merged &&
			fc_rel1->nparts == fc_rel2->nparts &&
			partition_bounds_equal(fc_part_scheme->partnatts,
								   fc_part_scheme->parttyplen,
								   fc_part_scheme->parttypbyval,
								   fc_rel1->boundinfo, fc_rel2->boundinfo))
		{
			fc_boundinfo = fc_rel1->boundinfo;
			fc_nparts = fc_rel1->nparts;
		}
		else
		{
			/* 尝试合并输入的分区边界。 */
			fc_boundinfo = partition_bounds_merge(fc_part_scheme->partnatts,
											   fc_part_scheme->partsupfunc,
											   fc_part_scheme->partcollation,
											   fc_rel1, fc_rel2,
											   fc_parent_sjinfo->jointype,
											   fc_parts1, fc_parts2);
			if (fc_boundinfo == NULL)
			{
				fc_joinrel->nparts = 0;
				return;
			}
			fc_nparts = list_length(*fc_parts1);
			fc_joinrel->partbounds_merged = true;
		}

		Assert(fc_nparts > 0);
		fc_joinrel->boundinfo = fc_boundinfo;
		fc_joinrel->nparts = fc_nparts;
		fc_joinrel->part_rels =
			(RelOptInfo **) palloc0(sizeof(RelOptInfo *) * fc_nparts);
	}
	else
	{
		Assert(fc_joinrel->nparts > 0);
		Assert(fc_joinrel->boundinfo);
		Assert(fc_joinrel->part_rels);

		/*
		 * 如果连接关系的 partbounds_merged 标志为 true，意味着输入
		 * 不保证具有相同的分区边界，因此我们不能假设在相同基数位置的分区
		 * 形成配对；让 get_matching_part_pairs() 生成配对。
		 * 否则，无需处理，因为我们可以假设。
		 */
		if (fc_joinrel->partbounds_merged)
		{
			get_matching_part_pairs(fc_root, fc_joinrel, fc_rel1, fc_rel2,
									fc_parts1, fc_parts2);
			Assert(list_length(*fc_parts1) == fc_joinrel->nparts);
			Assert(list_length(*fc_parts2) == fc_joinrel->nparts);
		}
	}
}

/*
 * get_matching_part_pairs
 *		从输入生成要连接的分区对。
 */
static void get_matching_part_pairs(PlannerInfo *fc_root, RelOptInfo *fc_joinrel,
						RelOptInfo *fc_rel1, RelOptInfo *fc_rel2,
						List **fc_parts1, List **fc_parts2)
{
	bool		fc_rel1_is_simple = IS_SIMPLE_REL(fc_rel1);
	bool		fc_rel2_is_simple = IS_SIMPLE_REL(fc_rel2);
	int			fc_cnt_parts;

	*fc_parts1 = NIL;
	*fc_parts2 = NIL;

	for (fc_cnt_parts = 0; fc_cnt_parts < fc_joinrel->nparts; fc_cnt_parts++)
	{
		RelOptInfo *fc_child_joinrel = fc_joinrel->part_rels[fc_cnt_parts];
		RelOptInfo *fc_child_rel1;
		RelOptInfo *fc_child_rel2;
		Relids		fc_child_relids1;
		Relids		fc_child_relids2;

		/*
		 * 如果此连接段为空，意味着在之前为其创建子连接路径的过程中
		 * 被忽略，因为它不会对连接结果产生贡献，原因是一个或两个输入为空；
		 * 向每个给定列表中添加 NULL，以便在该函数中再次忽略该段。
		 */
		if (!fc_child_joinrel)
		{
			*fc_parts1 = lappend(*fc_parts1, NULL);
			*fc_parts2 = lappend(*fc_parts2, NULL);
			continue;
		}

		/*
		 * 获取参与此连接段的来自 rel1 侧的分区的 relids 集合。
		 */
		fc_child_relids1 = bms_intersect(fc_child_joinrel->relids,
									  fc_rel1->all_partrels);
		Assert(bms_num_members(fc_child_relids1) == bms_num_members(fc_rel1->relids));

		/*
		 * 获取具有 relids 的 rel1 的子关系。请注意，即使 rel1 是一个连接关系，
		 * 我们还是应该有子关系，因为在这种情况下，relids 中指定的分区
		 * 将具有匹配/重叠的边界，因此在规划 rel1 的分区连接时，
		 * 应将指定的分区视为要连接的分区，这意味着在我们到达这一步时
		 * 子关系将已构建。
		 */
		if (fc_rel1_is_simple)
		{
			int			fc_varno = bms_singleton_member(fc_child_relids1);

			fc_child_rel1 = find_base_rel(fc_root, fc_varno);
		}
		else
			fc_child_rel1 = find_join_rel(fc_root, fc_child_relids1);
		Assert(fc_child_rel1);

		/*
		 * 获取参与此连接段的来自 rel2 侧的分区的 relids 集合。
		 */
		fc_child_relids2 = bms_intersect(fc_child_joinrel->relids,
									  fc_rel2->all_partrels);
		Assert(bms_num_members(fc_child_relids2) == bms_num_members(fc_rel2->relids));

		/*
		 * 获取具有 relids 的 rel2 的子关系。请参阅上面的注释。
		 */
		if (fc_rel2_is_simple)
		{
			int			fc_varno = bms_singleton_member(fc_child_relids2);

			fc_child_rel2 = find_base_rel(fc_root, fc_varno);
		}
		else
			fc_child_rel2 = find_join_rel(fc_root, fc_child_relids2);
		Assert(fc_child_rel2);

		/*
		 * rel1 和 rel2 的连接是合法的，因此上面获得的子关系的连接也是合法的；
		 * 将它们作为连接对添加到给定列表中，以产生此连接段。
		 */
		*fc_parts1 = lappend(*fc_parts1, fc_child_rel1);
		*fc_parts2 = lappend(*fc_parts2, fc_child_rel2);
	}
}
